/*
 *  Copyright (C) 1993  Robert Davis
 *
 *  This program is free software; you can redistribute it and/or
 *  modify it under the terms of Version 2, or any later version, of 
 *  the GNU General Public License as published by the Free Software 
 *  Foundation.
 */

static char RCSId[]="$Id: Gnuplot.m,v 1.12 1993/05/30 09:10:13 davis Exp $";


#import <appkit/Application.h>
#import <appkit/Listener.h>
#import <appkit/Matrix.h>
#import <appkit/Menu.h>
#import <appkit/MenuCell.h>
#import <appkit/OpenPanel.h>
#import <appkit/SavePanel.h>
#import <appkit/PrintInfo.h>
#import <appkit/Speaker.h>

#import <appkit/publicWraps.h>		/* NXConvertWinNumToGlobal()	*/

#import <objc/List.h>
#import <objc/NXStringTable.h>

#import <libc.h>			/* MAXPATHLEN			*/
#import <math.h>			/* M_PI				*/
#import <strings.h>			/* strcmp()			*/

#import "Gnuplot.h"
#import "GnuplotPlot.h"
#import "Inspector.h"
#import "Preferences.h"
#import "Version.h"

#define MAX_ID_LEN 50

extern struct value {			/* structs from plot.h		*/

    enum DATA_TYPES {INTGR, CMPLX} type;
    union {
	int		int_val;
	struct cmplx {
	    double real, imag;
	} cmplx_val;
    } v;

} *Gcomplex();

extern struct udvt_entry {

    struct udvt_entry	*next_udv;
    char		udv_name[MAX_ID_LEN+1];
    int			udv_undef;
    struct value	udv_value;

} *first_udv;



/*
 *  This function is from the NeXTSTEP Developer Example Draw by Paul 
 *  Hegarty.
 *  
 *  Sets the updateAction for every menu item that sends its action to 
 *  the First Responder (i.e. every menu item whose target is nil).  
 *  When autoupdate is on (which is always in this app), each event 
 *  will be followed by an update of every visible menu item.  This 
 *  keeps all unavailable menu items dimmed and disabled so that the 
 *  user knows what options are available at any given time.
 */ 
static void initMenu (Menu* menu)
{
    int count;
    Matrix *matrix;
    MenuCell *cell;
    id matrixTarget, cellTarget;

    matrix = [menu itemList];
    matrixTarget = [matrix target];

    count = [matrix cellCount];
    while (count--) {

        cell = [matrix cellAt:count :0];
        cellTarget = [cell target];

        if (!matrixTarget && !cellTarget)
            [cell setUpdateAction:@selector(menuItemUpdate:) forMenu:menu];
        else if ([cell hasSubmenu])
            initMenu(cellTarget);

    }
}


@implementation Gnuplot

+ setConstantUpdate:(int)updateType
{
    return [GnuplotPlot setConstantUpdate:updateType];
}


+ setHalvePlot:(BOOL)condition
{
    return [GnuplotPlot setHalvePlot:condition];
}


- init
{
    [super init];

    zone = [self zone];

    docList = [[List allocFromZone: zone] init];
    numUntitled = 1;
    isPoweringOff = NO;

    /* Set printinfo margins to 1/2 inch (36 points) on each side */
    [[NXApp printInfo] setMarginLeft:36.0 right:36.0 top:36.0 bottom:36.0];

    [NXApp setAutoupdate:YES];

    preferences = [[Preferences allocFromZone: zone] init];
    inspector = [[Inspector allocFromZone: zone] init];

    /*  Setup the first user-defined variable to be pi  */
    Gcomplex(&(first_udv->udv_value), M_PI, 0.0);

    return self;
}


// This is never called
- free
{
    [infoPanel free];
    [copyingPanel free];
    [inspector free];
    [preferences free];
    [stringSet free];
    [docList freeObjects];
    [docList free];

    return [super free];
}


- stringSet
{
    return stringSet;
}


- showInfoPanel:sender
{
    if (!infoPanel) {
	extern char	version[];
	char		rev[255];

	[NXApp loadNibSection: "InfoPanel.nib" 
                        owner: self
                    withNames: NO
	             fromZone: zone];

	/*  
	 *  nextstep_version is a string constant defined and 
	 *  initialized in Version.h that is automatically update by 
	 *  RCS.  It will contain "$Revision: " followed by the 
	 *  revision number.  The last character will be a '$'.  
	 *  version is a string defined in version.c and is only a 
	 *  number.
	 */

	strcpy (rev, nextstep_version);
	rev[strlen(rev) - 1] = '\0';			/* Remove last $ */
	[nextstepVersionTextField setStringValue:&(rev[11])];
	[versionTextField setStringValue:version];
    }

    [infoPanel makeKeyAndOrderFront:self];
    return self;
}



- showCopyingPanel:sender
{
    if (!copyingPanel)
	[NXApp loadNibSection: "CopyingPanel.nib"
                        owner: self
                    withNames: NO
                     fromZone: zone];

    [copyingPanel makeKeyAndOrderFront:self];

    return self;
}



- showInspectorPane:sender
{
    /* Inspector was created in -init */

    [inspector selectPane:[sender selectedTag]];
    [[inspector window] makeKeyAndOrderFront:self];
    return self;
}



- showPreferencesPanel:sender
{
    /* Preferences was created in -init */

    [preferences showPanel:self];
    return self;
}



- new:sender
{
    id newDoc = nil;

    if (newDoc = [[GnuplotPlot allocFromZone: zone] init])  {
	numUntitled++;
	[docList addObject:newDoc];
    }

    return newDoc;
}



- open:sender
{
    const char *const *files;
    static const char *const fileType[2] = {DOCUMENT_TYPE, NULL};
    OpenPanel *openPanel;
    id newDoc;
    char fullName[MAXPATHLEN];

    openPanel = [[OpenPanel new] allowMultipleFiles:YES];
    [openPanel setTitle:[stringSet valueForStringKey:"openT"]];

    /* Get a list of files to open from the user. */
    if ([openPanel runModalForTypes:fileType]) {

	/* For each pathname selected... */
	for (files = [openPanel filenames]; files && *files; files++) {

	    sprintf (fullName, "%s/%s", [openPanel directory], *files);

	    /* 
	     *  ... check to see if the doc is already opened.  If it 
	     *  is, bring it to the front.
	     */
	    if (newDoc = [self isDocOpen: fullName])
		[[newDoc window] makeKeyAndOrderFront:self];

	    /* If not, try to create a new one. */
	    else if ((newDoc = [[GnuplotPlot allocFromZone: zone]
					      initFromFile: fullName]))
		[docList addObject: newDoc];


	    /* 
	     *  Otherwise, just return.  GnuplotPlot will report the 
	     *  nature of the error to the user so we don't need to 
	     *  here.
	     */
	    else
		return nil;

	}

    }

    return self;
}



- saveAll:sender
{
    [docList makeObjectsPerform:@selector(save:) with:self];
    return self;
}


- setSaveType:sender
{
    id	savePanel = [SavePanel new];

    switch ([sender selectedTag]) {
    case SAVE_GNUPLOT:
	[savePanel setRequiredFileType:DOCUMENT_TYPE]; break;
    case SAVE_EPS:
	[savePanel setRequiredFileType:"eps"]; break;
    case SAVE_TIFF:
	[savePanel setRequiredFileType:"tiff"]; break;
    }

    return self;
}


- (int)saveType
{
    return [savePanelTypeMatrix selectedTag];
}


/* 
 *  This method should update any panels external to the doc which 
 *  reflect the current doc (e.g. the Inspector panel).
 */
- updateApp
{
    [inspector update];
    return self;
}




/*  
 *  The doc should send this when it closes so that we can remove it 
 *  from our doc list.
 */
- docDidClose:sender
{
    [docList removeObject:sender];
    [self setCurrentDoc:nil];
    return self;
}



/*  
 *  Checks the doc list to see if the doc specified by fullPath is 
 *  currently open.  If it is, the doc is returned.  Otherwise, nil is 
 *  returned.
 */
- isDocOpen: (const char *) fullPath
{
    int counter, docCount = [docList count];
    id aDoc;

    for (counter = 0 ; counter < docCount ; counter++)
	if (!strcmp ([aDoc = [docList objectAt: counter] name], fullPath))
	    return aDoc;

    return nil;
}




- (int)numberNew
{
    return numUntitled;
}



- setCurrentDoc:(GnuplotPlot *)aDoc
{
    if (aDoc)
	currentDoc = aDoc;
    else
	currentDoc = [[NXApp mainWindow] delegate];

    return self;
}


- (GnuplotPlot *)currentDoc
{
    return currentDoc;
}




/*** Auto-Update Methods ***/



/*
 *  This method is from the NeXTSTEP example Draw by Paul Hegarty.
 *
 *  When a menu is updated, this method is sent by each menu item that 
 *  sends its action to the First Responder.  If the object that would 
 *  respond to an action sent down the Responder chain also responds 
 *  to the validateCommand:, then we send validateCommand: to 
 *  determine whether the menu action is valid now.  Otherwise, if 
 *  there is a Responder to the message, then we assume that the item 
 *  is valid.  The method returns YES if the cell has changed its 
 *  appearance (so that the caller Menu knows to redraw it).
 */
- (BOOL)menuItemUpdate:menuCell
{
    SEL action;
    id responder, target;
    BOOL enable;

    target = [menuCell target];
    enable = [menuCell isEnabled];

    if (!target) {
	action = [menuCell action];
	responder = [NXApp calcTargetForAction:action];
	if ([responder respondsTo:@selector(validateCommand:)])
	    enable = [responder validateCommand:menuCell];
	else
	    enable = responder ? YES : NO;

    }

    if ([menuCell isEnabled] != enable) {
	[menuCell setEnabled:enable];
	return YES;
    } else
        return NO;

}


/*  
 *  This method is sent down the Responder chain by menuItemUpdate:
 *  
 *  Every object that responds to messages sent down the Responder 
 *  chain by menu cells but occasionally can't do perform the action 
 *  should implement validateCommand: and return a boolean value 
 *  telling whether the menu item should be enabled or not.
 *  
 *  For example:  we can respond to saveAll:, but we can't save all if 
 *  there are no docs that need saving.  So we need to implement 
 *  validateCommand: to  disable the saveAll: menu cell when it is 
 *  irrelevant.
 */
- (BOOL)validateCommand:menuCell
{
    SEL action = [menuCell action];
    int count;

    if (action == @selector(saveAll:))  {

	if ([docList count])
	    for (count = [docList count] - 1; count >= 0 ; count--)
		if ([[docList objectAt:count] validateCommand:menuCell])
		    return YES;
	    
	return NO;

    }

    return YES;
}







/*** Application Delegate Methods ***/


/* Sent when user opens one of our docs from the Workspace. */

- (int)app:sender openFile:(const char *)path type:(const char *)type
{
    id newObject;

    /* If the document is of the right type... */
    if (type && !strcmp (type, DOCUMENT_TYPE)) {

	/*  
	 *  ... first check to see if it's already open.  If it is, 
	 *  bring it to the front.
	 */
	if (newObject = [self isDocOpen: path])
	    [[newObject window] makeKeyAndOrderFront:self];


	/* Otherwise, attempt to open the document. */
	else if (newObject = [[GnuplotPlot allocFromZone: zone]
		                        initFromFile:path]) {

	    [docList addObject:newObject];

	    /*  
	     *  Set the OpenPanel directory so that when the user opens 
	     *  another doc, the open panel will start where he/she left
	     *  off.
	     */
	    [[OpenPanel new] setDirectory:path];

	    /* 
	     *  This causes documents opened from the Workspace to 
	     *  open more slowly, but if we don't the Inspector will 
	     *  be outdated until the next event.
	     */
	    [self updateApp];
	    return YES;

	}

    }

    return NO;
}


- app:sender powerOffIn:(int)ms andSave:(int)aFlag
{
    isPoweringOff = YES;
    return [self appWillTerminate:self];
}



- (BOOL)appAcceptsAnotherFile:sender
{
    /*  
     *  We return YES to this because we can open any number of 
     *  documents.
     */
    return YES;
}


- appDidInit:sender
{
    id	savePanel = [SavePanel new];
    int	inspectorPref;

    initMenu([NXApp mainMenu]);		/* Setup auto-menu-update	*/

    /*  
     *  Set services delegate to be self so this app can be a 
     *  service-provider and this object can handle the messages.
     */
    [[NXApp appListener] setServicesDelegate:self];


    /* Set save panel's accessory view */
    [savePanel setAccessoryView: [savePanelAccessory contentView]];
    [self setSaveType:savePanelTypeMatrix];

    /* 
     *  Open a new, untitled document, unless the app was started when 
     *  the user opened a document from the Workspace or the user has 
     *  specified not to with Preferences.
     */
    if (![docList count] && [preferences newDocument])
	[self setCurrentDoc:[self new:self]];

    /* 
     *  We make the inspector the key window down here, last, so that 
     *  it will be key when the user begins using the program.  Select 
     *  user's preferred pane, too.
     */
    inspectorPref = [preferences defaultInspector];
    if (inspectorPref != NO_INSPECTOR) {
	[inspector selectPane: inspectorPref];
	[[inspector window] makeKeyAndOrderFront:self];
    }

    return self;
}



- appWillTerminate: sender
{
    int editedCount, counter;
    id editedDocs = [[List allocFromZone: zone] init];
    id aDoc;

    do {

	/*  Build a list of edited, unsaved docs. */

	[editedDocs empty];
	counter = [docList count];
	while (--counter >= 0) {
	    aDoc = [docList objectAt: counter];
	    if ([aDoc isDocEdited])
		[editedDocs addObject: aDoc];
	}

	/*  
	 *  If there are edited, unsaved docs, ask if the user wants to 
	 *  review them, quit, or cancel the quit action (unless this 
	 *  method was sent by us during a poweroff/logoff, in which 
	 *  case we don't allow a cancel).
	 */
	if ((editedCount = [editedDocs count]) > 0)  {
	    switch (NXRunAlertPanel ([stringSet valueForStringKey: "quitT"],
				  [stringSet valueForStringKey: "unsavedDocs"],
				  [stringSet valueForStringKey: "reviewB"],
				  [stringSet valueForStringKey: "quitB"],
	      isPoweringOff? NULL: [stringSet valueForStringKey: "cancelB"])) {

	    case NX_ALERTDEFAULT:		/* Review Unsaved	*/

		counter = editedCount;
		while (--counter >= 0)
		    [[editedDocs objectAt: counter] close:self];
		break;

	    case NX_ALERTALTERNATE:		/* Quit Anyway		*/
		editedCount = 0;
		break;

	    case NX_ALERTOTHER:			/* Cancel		*/
		[editedDocs free];
		return nil;
		break;
	    }
	}

	/*  
	 *  If the user chooses to review unsaved, then is shown a Save 
	 *  panel for a doc and chooses "Cancel," we end up down here, 
	 *  where we'll repeat if there are more unedited docs.  
	 *  Basically, we force the user either to save all the docs or 
	 *  make a decision in the quit panel to review, quit, or 
	 *  cancel.
	 */
	
    } while (editedCount > 0);

    [editedDocs free];


    /*
     *  Quit the application.  (We free things here because -free is 
     *  never called -- NXApp simply exit()'s -- but GnuplotPlot's 
     *  implementation of -free deletes the plot's tmp file, so we 
     *  must call it to avoid leaving junk in /tmp.) 
     */
    
    [docList makeObjectsPerform: @selector(free)];
    [docList free];

    return self;
}



/*  
 *  This method is sent when a user chooses one of Gnuplot's services 
 *  from the Services menu in another application.
 */
- plotData:pb userData:(const char *)userData error:(char **)errorMessage
{
    char	*data;
    int		length, i;
    const char	*const *types;
    BOOL	hasFilename;

    types = [pb types];
    hasFilename = NO;
    for (i=0 ; !hasFilename && types[i] ; i++)
	if (!strcmp (types[i], NXFilenamePboardType)) hasFilename = YES;

    if (hasFilename) {
        [pb readType:NXFilenamePboardType data:&data length:&length];

	if (data && length) {
	    BOOL	useCurrent = (currentDoc && strcmp (userData, "new"));
	    char	*path = NXCopyStringBufferFromZone (data, zone);
	    char	*beg, *end;
	    BOOL	atEnd;

	    /*  
	     *  If there is a current document, we add each data file 
	     *  specified on the pasteboard to it.  If there's no
	     *  current document, or if the userData contains "new" 
	     *  we create a new document and add the data file to 
	     *  that.
	     */

	    beg = path;
	    atEnd = beg? NO:YES;
	    while (!atEnd) {
		end = index (beg, '\t');

		if (end && *end && *end == '\t')
		    *end = '\0';
		else
		    atEnd = YES;

		if (useCurrent)
		    [currentDoc addDataFile:beg];
		else {
		    id	newDoc = [self new:self];
		    [newDoc addDataFile:beg];
		    [self setCurrentDoc:newDoc];
		    useCurrent = YES;	/* Just create one new doc for all */
		}

		if (!atEnd) {
		    *end = '\t';
		    beg = end + 1;
		}
	    }

	    NXZoneFree(zone, path);
	    [self updateApp];
	}
    }

    return self;
}


// Shuts up the compiler about unused RCSId
- (const char *) rcsid
{
    return RCSId;
}


@end

